package Engine;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

import Engine.InputHandling.Keyboard;
import Engine.InputHandling.Mouse;
import Engine.TileHandling.Tile;
import Engine.TileHandling.TileRenderer;
import Engine.Utils.ImageHandler;
import Engine.Utils.Time;
import Engine.Utils.Vector2f;

/**
 * This is the main class of the engine, which handles the logic and rendering methods.
 * You either can use the default rendering methods for tiles or write your own renderer.
 * @author Lueko
 *
 */
public abstract class Engine extends Canvas implements Runnable {

	private static final long serialVersionUID = 1L;
	
	/**
	 * True if the "heart" thread is running, false otherwise.
	 */
	protected boolean running;

	/**
	 * Width and height of the canvas.
	 */
    protected int width, height;
    
    /**
     * Main window.
     */
	protected Window window;
	
	/**
	 * Max. frames per second (doesn't effect the accuracy).
	 */
	protected int FRAME_CAP = 120;
	
	/**
	 * Maximum updates per second (defines the number of acts per second).
	 */
	protected int UPDATE_CAP = 60;
	
	/**
	 * Screen time of a single frame in seconds.
	 */
	protected double frameTime;
	
	/**
	 * Time per update in nanoseconds.
	 */
	protected double ns;
	
	/**
	 * Current frames per second.
	 */
	protected int fps;
	
	/**
	 * Current updates per second.
	 */
	protected int ups;
	
	/**
	 * Image to draw in.
	 * Is drawn into the canvas.
	 */
	protected BufferedImage image;
	protected static BufferStrategy bs;
	
	/**
	 * Possesses all objects which need to be drawn.
	 */
	protected static ArrayList<Drawable> drawables = new ArrayList<Drawable>();
	
	/**
	 * Possesses all objects which need to do logic.
	 */
	protected static ArrayList<Updateable> updateables = new ArrayList<Updateable>();
	
	/**
	 * Width and height of the world tile grid.
	 */
	protected int worldWidth, worldHeight;
	
	/**
	 * Colors of every tile in the world (worldWidth * worldHeight).
	 */
	protected int[] tiles;
	
	/**
	 * Current position of the world camera.
	 */
	protected static Vector2f mapPos;
	
	/**
	 * Controls the rendering of tiles.
	 */
	protected TileRenderer tileRenderer;
	
	/**
	 * Moving speed of the world camera.
	 */
	protected float moveViewSpeed = 4.8f;
	
	/**
	 * Sets whether to use the included moving system.
	 */
	protected boolean moveView;
	
	/**
	 * Controls keyboard input.
	 */
	protected Keyboard keyboard;
	
	/**
	 * Controls mouse input.
	 */
	protected Mouse mouse;

	/**
	 * If you use this constructor, you have to overwrite the "renderStuff(Graphics g)" method.
	 * @param title Title of the window.
	 * @param width Width of the canvas.
	 * @param height Height of the canvas.
	 */
	public Engine(String title, int width, int height) {
	    init(title, width, height);
	    
	    requestFocus();
	    
	    start();
	}
	
	/**
	 * @param title Title of the window.
	 * @param width Width of the canvas.
	 * @param height Height of the canvas.
	 * @param mapPath Path of the image with the map data stored.
	 */
    public Engine(String title, int width, int height, String mapPath) {
	    moveView = true;
	    
	    init(title, width, height);
        
        loadMap(mapPath);
        
        tileRenderer = new TileRenderer(width, height, worldWidth, worldHeight, tiles);
        
        requestFocus();
        
        start();
	}

    /**
     * Basic initialization for every new Engine.
     * @param title Title of the window.
     * @param width Width of the canvas.
     * @param height Height of the canvas.
     */
    private void init(String title, int width, int height) {
    	this.width = width;
	    this.height = height;
	    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        mapPos = new Vector2f(0, 0);
        keyboard = new Keyboard();
        mouse = new Mouse();
        frameTime = 1.0 / FRAME_CAP;
        ns = Time.SECOND / UPDATE_CAP;
        setPreferredSize(new Dimension(width, height));
        window = new Window(title, this);
        addKeyListener(keyboard);
        addMouseListener(mouse);
        addMouseMotionListener(mouse);
        addMouseWheelListener(mouse);
    }
    
    /**
     * Starts the thread (run method).
     */
	public synchronized void start() {
	    Thread th = new Thread(this);
	    th.start();
	}
	
	/**
	 * Life loop of the engine.
	 */
	@Override
	public void run() {
        running = true;
        
        int updates = 0;
        int frames = 0;
        long frameCounter = 0;
        
        double delta = 0;
        
        long startTime;
        long passedTime;
        long lastTime = Time.getTime();
        double unprocessedTime = 0;
        boolean render = false;
        int clickedTime = 0;
        
        while (running) {
        	if (Mouse.smthClicked()) clickedTime++;
            startTime = Time.getTime();
            passedTime = startTime - lastTime;
            lastTime = startTime;
            
            unprocessedTime += passedTime / (double) Time.SECOND;
            frameCounter += passedTime;
            

            delta += passedTime / ns;
            while (delta >= 1) {
                update();
                updates++;
                delta--;
            }
            
            if (frameCounter >= Time.SECOND) {
                fps = frames;
                ups = updates;
                frames = 0;
                frameCounter = 0;
                updates = 0;
            }
            
            render = false;
            while (unprocessedTime > frameTime) {
                render = true;
                unprocessedTime -= frameTime;
            }

            if (render) {
                renderImage();
                frames++;
            } else {
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (clickedTime >= 25) {
            	Mouse.resetClick();
            	clickedTime = 0;
            }
        }
	}
	
	/**
	 * Main update method. Calls the updateStuff method and handles all updateables and moves the camera
	 * if activated.
	 */
	protected void update() {
	    if (moveView) moveView();
	    
	    updateStuff();
	    
	    for (int i = updateables.size() - 1; i >= 0 ; i--) {
            if(!updateables.get(i).removed()) {
                updateables.get(i).tick();
            } else {
                updateables.remove(i);
            }
        }
	    
	    if (Keyboard.keysDown[KeyEvent.VK_ESCAPE]) {
	        System.exit(0);
	    }
	}
	
	/**
	 * Update method to overwrite. Will be called in the main update method.
	 */
	protected abstract void updateStuff();
	
	/**
	 * Main render method. Renders drawables and calls the renderStuff method.
	 */
	protected void render() {
	    Graphics g = getImageGraphics();
        
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, width, height);
        
        renderStuff(g);
        
        renderDrawables(g);
	}
	
	/**
	 * Renders all drawables by default.
	 */
	protected void renderDrawables(Graphics g) {
		for (int i = drawables.size() - 1; i >= 0 ; i--) {
            Drawable drawable = drawables.get(i);
            if(!drawable.removed()) {
                drawable.render(g);
            } else {
                drawables.remove(drawable);
            }
        }
	}
	
	/**
     * Renders tiles by default.
     */
	protected void renderStuff(Graphics g) {
        tileRenderer.renderTiles(g, mapPos);
	}
	
	/**
	 * Loads the map image and colors of the pixels to the tiles array.
	 * [Tileworld only]
	 * @param path Path of the image.
	 */
	protected void loadMap(String path) {
        BufferedImage image = ImageHandler.load(path);
        worldWidth = image.getWidth();
        worldHeight = image.getHeight();
        tiles = new int[worldWidth * worldHeight];
        image.getRGB(0, 0, worldWidth, worldHeight, tiles, 0, worldWidth);
    }
	
	/**
	 * @return Graphics object of the world image rendered on.
	 */
	protected Graphics getImageGraphics() {
	    return image.createGraphics();
	}

	/**
	 * Renders the world image into the canvas with triple buffering.
	 */
	private void renderImage() {
	    bs = getBufferStrategy();
        if (bs == null) {
            createBufferStrategy(3);
            return;
        }
        
        render();
        
        Graphics g = bs.getDrawGraphics();
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, getWidth(), getHeight());
        g.drawImage(image, 0, 0, null);
        g.dispose();
        bs.show();
	}
	
	/**
	 * Moves the camera of the world, if a key is pressed (w, a, s, d).
	 * [Tileworld only]
	 */
	private void moveView() {
        if (mapPos.getX() - moveViewSpeed >= 0) {
            if (Keyboard.keysDown[KeyEvent.VK_A]) mapPos.setX(mapPos.getX() - moveViewSpeed);
        }
        if (mapPos.getY() - moveViewSpeed >= 0) {
            if (Keyboard.keysDown[KeyEvent.VK_W]) mapPos.setY(mapPos.getY() - moveViewSpeed);
        }
        if (mapPos.getX() + moveViewSpeed + width <= worldWidth * Tile.SIZE) {
            if (Keyboard.keysDown[KeyEvent.VK_D]) mapPos.setX(mapPos.getX() + moveViewSpeed);
        }
        if (mapPos.getY() + moveViewSpeed + height <= worldHeight * Tile.SIZE) {
            if (Keyboard.keysDown[KeyEvent.VK_S]) mapPos.setY(mapPos.getY() + moveViewSpeed);
        }
    }
	
	/**
	 * @return Drawables ArrayList.
	 */
	public static ArrayList<Drawable> getDrawables() {
	    return drawables;
	}
	
	/**
	 * @return Updateables ArrayList.
	 */
	public static ArrayList<Updateable> getUpdateables() {
        return updateables;
    }

	/**
	 * @return Width of the canvas.
	 */
    public int getWidth() {
        return width;
    }

    /**
     * @return Height of the canvas.
     */
    public int getHeight() {
        return height;
    }

    /**
     * @return The window.
     */
    public Window getWindow() {
        return window;
    }
    
    /**
     * @return Position of the camera as a Vector2f.
     */
    public static Vector2f getMapPos() {
        return mapPos;
    }

    /**
     * Sets the position of the camera.
     * @param mapPos Position to set as a Vector2f.
     */
    public static void setMapPos(Vector2f mapPos) {
        Engine.mapPos = mapPos;
    }

    /**
     * [Tileworld only]
     */
    public int getWorldWidth() {
        return worldWidth;
    }

    /**
     * [Tileworld only]
     */
    public int getWorldHeight() {
        return worldHeight;
    }

    /**
     * [Tileworld only]
     */
    public int[] getTiles() {
        return tiles;
    }
    
    public float getMoveViewSpeed() {
        return moveViewSpeed;
    }

    public void setMoveViewSpeed(int moveViewSpeed) {
        this.moveViewSpeed = moveViewSpeed;
    }

    /**
     * [Tileworld only]
     */
    public boolean isMoveView() {
        return moveView;
    }

    /**
     * [Tileworld only]
     */
    public void setMoveView(boolean moveView) {
        this.moveView = moveView;
    }
    
    public boolean isRunning() {
        return running;
    }

    public void setRunning(boolean running) {
        this.running = running;
    }

	public int getFrameCap() {
		return FRAME_CAP;
	}

	public void setFrameCap(int FRAME_CAP) {
		this.FRAME_CAP = FRAME_CAP;
		frameTime = 1.0 / FRAME_CAP;
	}

	public int getUpdateCap() {
		return UPDATE_CAP;
	}

	public void setUpdateCap(int UPDATE_CAP) {
		this.UPDATE_CAP = UPDATE_CAP;
        ns = 1000000000.0 / UPDATE_CAP;
	}
	
}
